package nl.Viewer.ViewManager.Views;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;

import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLJPanel;
import javax.media.opengl.glu.GLU;

import nl.Viewer.SerializedObjects.Parameter;
import nl.Viewer.Sessions.Communication.ParameterException;
import nl.Viewer.ViewManager.Controllers.GenericController;
import nl.Viewer.ViewManager.Models.DataStores.ImageDataObject;

/**
 * An implementation of a view which shows a (possibly rendered) image. The view allows panning and zooming.
 * 
 * @author Jeroen Verschuur
 * 
 */

public class JOGLImageDataObjectAsImageView extends GenericView implements GLEventListener, MouseListener,
      MouseMotionListener, MouseWheelListener {
   private static final long serialVersionUID        = -2958949544243327703L;

   /********************************************************************************************************************
    * 
    * Interaction properties
    * 
    */

   protected Point           lastLocation            = null;
   // set to true if dragging the mouse pointer out of the panel
   private boolean           dragOutOfScreen         = true;
   /* viewing parameters */
   protected boolean         interactionAllowed      = false;

   protected Parameter       VisibleRectangle;
   protected double          visibleRectangleValue[] = new double[] { 0.0, 0.0, 1.0, 1.0 };
   protected Parameter       ImageDimension;
   protected int             imageDimensionValue[]   = new int[] { 0, 0 };

   private boolean           parametersRetrieved     = false;

   /********************************************************************************************************************
    * 
    */
   protected double          aspect                  = 1.0;
   protected boolean         interacting             = false;
   protected GLU             glu                     = new GLU();
   protected ImageDataObject buf                     = null;

   protected double          minScale                = 1.0;
   protected double          scale                   = 1.0;
   protected Double          imageCenterX            = 0.0, imageCenterY = 0.0;

   public JOGLImageDataObjectAsImageView( GenericController parent ) {
      super( parent );
      this.renderPanel = new GLJPanel( getCapabilities() );

      ( (GLJPanel)renderPanel ).addGLEventListener( this );
      renderPanel.addMouseListener( this );
      renderPanel.addMouseMotionListener( this );
      renderPanel.addMouseWheelListener( this );

      parent.getDataModel().addParameterModelChangeListener( this );
      parent.getDataModel().addDataStoreModelChangeListener( this );

   }

   public static GLCapabilities getCapabilities() {
      GLCapabilities c = new GLCapabilities();
      c.setDoubleBuffered( true );
      return c;
   }

   /**
    * Called by drawable to indicate mode or device has changed
    */
   public void displayChanged( GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged ) {
   }

   /**
    * Called after OpenGL is init'ed
    */
   public void init( GLAutoDrawable drawable ) {
      GL gl = drawable.getGL();
      // set erase color
      gl.glClearColor( 0.0f, 0.0f, 0.0f, 0.0f ); // white
      // set drawing color and point size
      gl.glColor3f( 0.0f, 0.0f, 0.0f );
      gl.glPointSize( 1.0f ); // a 'dot' is 4 by 4 pixel

      // enable alpha mask (import from gif sets alpha bits)
      gl.glBlendFunc( GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA );
      gl.glEnable( GL.GL_BLEND );
   }

   private boolean hasDrawn  = false;

   private int     oldX      = -1;
   private int     oldY      = -1;
   private int     oldWidth  = -1;
   private int     oldHeight = -1;

   /**
    * Called to indicate the drawing surface has been moved and/or resized
    */
   public void reshape( GLAutoDrawable drawable, int x, int y, int width, int height ) {
      GL gl = drawable.getGL();
      gl.glViewport( 0, 0, width, height );
      gl.glMatrixMode( GL.GL_PROJECTION );
      gl.glLoadIdentity();
      gl.glDisable( GL.GL_DEPTH_TEST );
      aspect = (double)width / (double)height;
      glu.gluOrtho2D( 0.0, width, 0.0, height );

      resetScale();

      if( hasDrawn && ( x != oldX || y != oldY || width != oldWidth || height != oldHeight ) ) {
         // requestNewRender();
      }
      oldX = x;
      oldY = y;
      oldWidth = width;
      oldHeight = height;
   }

   public void display() {
      ( (GLJPanel)renderPanel ).display();
   }

   /**
    * Called by drawable to initiate drawing
    */
   public void display( GLAutoDrawable drawable ) {
      GL gl = drawable.getGL();
      gl.glClear( GL.GL_COLOR_BUFFER_BIT );

      // 2D
      // gl.glMatrixMode( GL.GL_PROJECTION );
      // gl.glLoadIdentity();
      //      
      // glu.gluOrtho2D( 0.0, panelWidth, 0.0, panelHeight );

      gl.glMatrixMode( GL.GL_MODELVIEW );
      gl.glLoadIdentity();

      drawImage( gl );

      gl.glFlush();
      ( (GLJPanel)renderPanel ).swapBuffers();
   }

   public synchronized void drawImage( GL gl ) {
      hasDrawn = true;

      if( buf == null || buf.data == null )
         return;

      /*
       * calculate the offset needed to get the requested imageCenter in the center of the frame
       */
      Dimension frameDimension = ( (GLJPanel)renderPanel ).getSize();

      double scaledImageCenterX = imageCenterX * scale;
      double scaledImageCenterY = imageCenterY * ( scale );

      int screenCenterX = frameDimension.width / 2;
      int screenCenterY = frameDimension.height / 2;

      Double xOffset = screenCenterX - scaledImageCenterX;
      Double yOffset = screenCenterY - scaledImageCenterY;

      /*
       * NOTES To set a valid raster position outside the viewport, first set a valid raster position inside the
       * viewport, then call glBitmap with NULL as the bitmap parameter and with xmove and ymove set to the offsets of
       * the new raster position. This technique is useful when panning an image around the viewport.
       */
      gl.glPixelZoom( (float)scale, (float)scale );

      gl.glRasterPos2d( 0, 0 );
      gl.glBitmap( 0, 0, 0, 0, xOffset.floatValue(), yOffset.floatValue(), null );

      // System.out.println( "WxH: " + imageWidth + "x" + imageHeight + ", xOffset/yOffset: " + xOffset + "/" + yOffset
      // + ", scale=" + scale );

      if( interacting ) {
         gl.glPixelTransferf( GL.GL_RED_SCALE, 0.3f );
         gl.glPixelTransferf( GL.GL_GREEN_SCALE, 0.3f );
         gl.glPixelTransferf( GL.GL_BLUE_SCALE, 0.3f );
      }

      // we should always synchronize on the buffer to prevent access by for example the DataStore or other views
      synchronized( buf ) {
         buf.data.rewind();
         gl.glDrawPixels( buf.width, buf.height, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, buf.data );
      }

      if( interacting ) {
         gl.glPixelTransferf( GL.GL_RED_SCALE, 1.0f );
         gl.glPixelTransferf( GL.GL_GREEN_SCALE, 1.0f );
         gl.glPixelTransferf( GL.GL_BLUE_SCALE, 1.0f );
      }

   }

   /*
    * Parameter methods
    * 
    */

   public void parameterValueChanged( Parameter p ) {
      synchronized( this ) {
         synchronizeParameterValues();
      }
   }

   public void dataStoreChanged( boolean newDataStore ) {
      synchronized( this ) {
         if( newDataStore ) {
            activateDataStore();
         } else {
            // ignore, do nothing for now
         }
      }
   }

   private void getAllParameters() {
      if( parametersRetrieved )
         return;

      // ConcurrentHashMap<String, Parameter> parameters = parent.getParameters();

      // add a parameter for the visible area
      try {
         VisibleRectangle =
            new Parameter( "VisibleRectangle", Parameter.ParameterType.DOUBLE, false, 4, new Double[] { 0.0, 0.0, 1.0,
               1.0 }, false, false, false );

         parent.getDataModel().addParameter( VisibleRectangle );

         ImageDimension =
            new Parameter( "ImageDimension", Parameter.ParameterType.INT, false, 2, new Double[] { 0.0, 0.0 }, false,
                  false, false );

         parent.getDataModel().addParameter( ImageDimension );

      }
      catch( ParameterException pe ) {
         System.err.println( "Parameter VisibleRectangle could not be added for the ImageDataObjectToImage View" );
      }

      parametersRetrieved = true;

   }

   private void synchronizeParameterValues() {
      super.synchronizeViews();      
   }

   private void calculateVisibleRectangle() {
      // calculate the visible rectangle in image space
      Dimension panelSize = this.getRenderPanel().getSize();
      double halfVisibleWidth = ( panelSize.width / this.scale ) / 2;
      double halfVisibleHeight = ( panelSize.height / this.scale ) / 2;

      // [x1, y1, x2, y2]
      visibleRectangleValue[0] = this.imageCenterX - halfVisibleWidth;
      visibleRectangleValue[1] = this.imageCenterY - halfVisibleHeight;
      visibleRectangleValue[2] = this.imageCenterX + halfVisibleWidth;
      visibleRectangleValue[3] = this.imageCenterY + halfVisibleHeight;

      if( buf != null ) {
         // check if the rectangle is bigger than the image itself, otherwise clip it
         if( visibleRectangleValue[0] < 0 )
            visibleRectangleValue[0] = 0;
         if( visibleRectangleValue[1] < 0 )
            visibleRectangleValue[1] = 0;
         if( visibleRectangleValue[2] > buf.width )
            visibleRectangleValue[2] = buf.width;
         if( visibleRectangleValue[3] > buf.height )
            visibleRectangleValue[3] = buf.height;

         imageDimensionValue[0] = buf.width;
         imageDimensionValue[1] = buf.height;
      }

      // update the parameter
      VisibleRectangle.setParameterValue( visibleRectangleValue );
      VisibleRectangle.setModified( true );

      ImageDimension.setParameterValue( imageDimensionValue );

      parent.getDataModel().forceParameterUpdateNotification( VisibleRectangle, ImageDimension );

   }

   /*
    * This method sets all parameters before each render
    */
   public void setPreRenderParameters() {
      // this should actually be done in the constructor, however, the list of parameters is not yet retrieved when this
      // class is constructed

      // getAllParameters will set parametersRetrieved to true in case this was the first render
      getAllParameters();

      calculateVisibleRectangle();
   }

   public synchronized void activateDataStore() {
      Object currentData = parent.getDataStore().getCurrentData();
      if( currentData == null ) {
         System.out.println( "No datastore available yet" );
      } else {
         if( currentData instanceof ImageDataObject == false ) {
            System.out.println( "MUST have ImageDataObject's in datastore to use this renderer (we have "
               + currentData.getClass() + ")" );
         } else {
            setBuffer( (ImageDataObject)currentData );
         }
      }
   }

   private void setBuffer( ImageDataObject ido ) {
      imageCenterX = (double)ido.width / 2.0;
      imageCenterY = (double)ido.height / 2.0;

      buf = ido;

      // get new viewing parameters from the server
      synchronizeParameterValues();

      resetScale();
      interactionAllowed = true;
   }

   public void resetScale() {
      // determine a suitable scale factor (never scale up,
      // use smallest scale factor for x- or y-scaling so the image will
      // always fit)

      scale = 1.0; // we should never scale up automatically

      if( buf != null ) {
         Dimension curSize = ( (GLJPanel)renderPanel ).getSize();
         double xscale = (double)curSize.width / (double)( ( ( buf.width / 2 ) - imageCenterX ) + buf.width );
         double yscale = (double)curSize.height / (double)( ( ( buf.height / 2 ) - imageCenterY ) + buf.height );

         if( xscale < scale ) {
            scale = xscale;
         }
         if( yscale < scale ) {
            scale = yscale;
         }
      }
      System.out.println( "Image Panel scale: " + scale );

      minScale = scale;

      setScaleInFrameTitle();
      // just for this view: update the visible rectangle with the pre-render parameters
      setPreRenderParameters();
   }

   private void setScaleInFrameTitle() {
      float percentage = (float)( ( (int)( scale * 10000.0 ) ) / 100.0 );

      parent.setFrameTitle( "(" + percentage + " %)" );
   }

   public void moveUp( double up ) {
      if( buf == null )
         return;

      double tempCenter = imageCenterY - ( up / scale );

      if( tempCenter <= -( buf.height / 2.0 ) ) {
         imageCenterY = -( buf.height / 2.0 ) + 1.0;
      } else if( tempCenter >= ( buf.height + ( buf.height / 2.0 ) ) ) {
         imageCenterY = buf.height + ( buf.height / 2.0 ) - 1.0;
      } else {
         imageCenterY = tempCenter;
      }
      // just for this view: update the visible rectangle
      setPreRenderParameters();
   }

   public void moveRight( double right ) {
      if( buf == null )
         return;

      double tempCenter = imageCenterX - ( right / scale );

      if( tempCenter <= -( buf.width / 2.0 ) ) {
         imageCenterX = -( buf.width / 2.0 ) + 1.0;
      } else if( tempCenter >= ( buf.width + ( buf.width / 2.0 ) ) ) {
         imageCenterX = ( buf.width + ( buf.width / 2.0 ) ) - 1.0;
      } else {
         imageCenterX = tempCenter;
      }

      // just for this view: update the visible rectangle with the render parameters
      setPreRenderParameters();
   }

   public void scale( double factor ) {
      double tempScale = scale * factor;
      if( tempScale < minScale ) {
         this.scale = minScale;
      } else {
         this.scale = tempScale;
      }
      setScaleInFrameTitle();
      // just for this view: update the visible rectangle
      setPreRenderParameters();
   }

   /*
    * Interaction methodes
    * 
    */

   public void mouseReleased( MouseEvent event ) {
      // reset last mouse location
      lastLocation = null;
      interacting = false;

      // request render
      // parent.requestNewRender(); // do nothing
   }

   public void mousePressed( MouseEvent event ) {
      // save last location on screen
      // interacting = true;
      lastLocation = event.getLocationOnScreen();
   }

   public void mouseDragged( MouseEvent event ) {
      if( !dragOutOfScreen && !( (GLJPanel)renderPanel ).contains( event.getPoint() ) ) {
         return;
      }

      if( lastLocation == null )
         return; // this should/can not happen!

      Point p = event.getLocationOnScreen();

      int xDist = ( p.x - lastLocation.x );
      int yDist = ( p.y - lastLocation.y );

      // check which mouse button was used
      if( ( event.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK ) == MouseEvent.BUTTON1_DOWN_MASK ) {
         this.moveRight( xDist );
         this.moveUp( -yDist );
      }

      if( ( event.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK ) == MouseEvent.BUTTON3_DOWN_MASK ) {
         double relative = 1 - ( (double)yDist / (double)( (GLJPanel)renderPanel ).getHeight() );

         scale( relative );
      }

      lastLocation = event.getLocationOnScreen();

      display();
   }

   public void mouseWheelMoved( MouseWheelEvent event ) {
      // translateZ += (float)event.getWheelRotation();
      scale( 1.0 - (double)event.getWheelRotation() * 0.1 );
      display();
   }

}
